1、概述
递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,
动态规划保存了子问题的解,避免重复计算。
动态规划一般可分为4类:
- 线性动规(Sequence/Two Sequences DP)
- 区域动规(Matrix DP)
- 树形动规 (Tree DP)
- 背包动规(Backpack)
2、矩阵类型
2.1 矩形路径最小和
public static int minPathSum(int[][] matrix) {
int minPath = 0;
if (matrix == null || matrix[0].length == 0 ||matrix.length == 0){
return minPath;
}
int width = matrix[0].length;
int height = matrix.length;
for (int i = 1; i < width; i++) {
matrix[0][i] = matrix[0][i-1] + matrix[0][i];
}
for (int i = 1; i < height; i++) {
matrix[i][0] = matrix[i-1][0] + matrix[i][0];
}
for (int i = 1; i < height ; i++) {
for (int j = 1; j < width; j++) {
matrix[i][j] = Math.min(matrix[i][j-1], matrix[i-1][j]) + matrix[i][j];
}
}
return matrix[height-1][width-1];
}
2.2 m*n棋盘走法
- 递归实现
给定一个M×N的格子或棋盘,从左下角走到右上角的走法总数(每次只能向右或向上移动一个方格边长的距离: f(m,n)=f(m-1,n)+f(m,n-1)
于是状态f(i,j)的状态转移方程为:
f(i,j)=f(i-1,j)+f(i,j-1) if i,j>0
f(i,j)=f(i,j-1) if i=0
f(i,j)=f(i-1,j) if j=0
f(0,0)=0, f(0,1)=1, f(1,0)=1
public static int process(int m,int n){
if(m==0&&n==0){
return 0;
}
if(m==1||n==1){
return 1;
}
return process(m,n-1)+process(m-1,n);
}
- 动态规划(不存在障碍物)
// 不存在障碍物
// 每个位置的个数= right + down
public static int uniquePaths(int m, int n){
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
dp[i][j]= i*j == 0 ? 1:dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
- 动态规划(存在障碍物)
// 躲避障碍物
// 每个位置的个数= right + down
// 障碍时直接置为0
public static int uniquePathsObstacleGrid(int[][] obstacleGrid) {
if (obstacleGrid[0][0] == 1) {
return 0;
}
int width = obstacleGrid[0].length;
int height = obstacleGrid.length;
// 构造dp,初始化
int[][] dp = new int[height][width];
dp[0][0] = 1;
for (int i = 1; i < height; i++) {
dp[i][0] = obstacleGrid[i][0] == 1? 0:dp[i-1][0];
}
for (int i = 1; i < width; i++) {
dp[0][i] = obstacleGrid[0][i] == 1? 0:dp[0][i-1];
}
for (int i = 1; i < height; i++) {
for (int j = 1; j < width; j++) {
dp[i][j] = obstacleGrid[i][j] == 1? 0:dp[i-1][j] + dp[i][j-1];
}
}
return dp[height-1][width-1];
}
2、序列类型
2.1 爬楼梯
2.1.1 简单爬楼梯
- 递归实现
// 时间复杂度为O(2^n)
// 空间复杂度为O(n)
public int fib( int n ){
if( n == 0 )
return 0;
if( n == 1 )
return 1;
return fib(n-1) + fib(n-2);
}
- 动态规划
思路:n层楼梯,最后一步走完可能是1或2:f(n) = f(n-1) + f(n-2)
自己计算当楼梯数为1、2、3、4、5时,对应的爬法有:1、2、3、5、8、13、21种。 可以发现,随着楼梯数n的增加,爬法总数呈现斐波那契数列规律增加,
即f(n) = f(n-1) + f(n-2)
// 时间复杂度O(n)
// 空间复杂度O(1)
public int fibonacci(int n) {
if (n == 0)
return 0;
if (n == 1 || n == 2)
return 1;
int f1 = 1, f2 = 1, fn;
for (int i = 3; i <= n; i++) {
fn = f1 + f2;
f1 = f2;
f2 = fn;
}
return fn;
}
// 或
public static int climbStairs2(int n){
if (n == 1 || n == 2){
return n;
}
int[] dp = new int[n+1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n ; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
2.1.2 跳到尾部
public static boolean canJumps(int[] arr){
if (arr.length == 0){
return true;
}
boolean[] dp = new boolean[arr.length];
dp[0] = true;
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < i ; j++) {
if (dp[j] && arr[j] + j >= i){
dp[i] = true;
}
}
}
return dp[arr.length-1];
}
2.1.3 猴子吃桃子
/**
* n个桃子,每天吃掉一半多一个
*
* 第10天剩下1个,总共多少桃子
* n 为天数
*
* f(n-1)= f(n)/2-1
*
* f(n) = 2* f(n-1) + 2
*
*/
public static int monkeyQue(int x) {
if (x == 0 || x == 1){
return x;
}
int sum = 1;
for (int i = 1; i <= x; i++) {
sum = (sum+1)*2;
}
return sum;
}
2.2 最长递增子序列长度
2.2.1 方法1(nlogn)
初始时,nums=[10,9,2,5,3,7,101,18],res=[]
i=0, res=[10]
i=1, res=[9]
i=2, res=[2]
i=3, res=[2,5]
i=4, res=[2,3]
i=5, res=[2,3,7]
i=6, res=[2,3,7,101]
i=7, res=[2,3,7,18]
小于替换大于添加
public static int lengthOfLIS(int[] nums) {
int len = nums.length;
if (len <= 1) {
return len;
}
int[] dp = new int[len];
dp[0] = nums[0];
int end = 0;
for (int i = 1; i < len; i++) {
if (nums[i] > dp[end]) {
end++;
dp[end] = nums[i];
} else {
int left = 0;
int right = end;
while (left < right) {
int mid = left + ((right - left) >>> 1);
if (dp[mid] < nums[i]) {
left = mid + 1;
} else {
right = mid;
}
}
dp[left] = nums[i];
}
}
end++;
return end;
}
2.2.2 方法2:dp
public static int lengthOfLIS(int[] arr){
if (arr.length < 2){
return arr.length;
}
int[] dp = new int[arr.length];
dp[0] = 1;
int res = 1;
for (int i = 1; i < arr.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (arr[j] < arr[i]){
dp[i] = Math.max(dp[i], dp[j]+1);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
2.3 字符串最长的回文长度
1、中心扩展
2、动态规划
/**
* 最长回文串-动态规划解法
* 例:abcbcca
* start maxLen curLen
*
* a 0 1 1
* ab 1 1 2
*
* @return
*/
public static String longestPalindrome(String str) {
if (str == null || str.length() < 2) {
return str;
}
int len = str.length();
int start = 0;
int maxLen = 1;
boolean[][] dp = new boolean[len][len];
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
for (int j = 1; j < len; j++) {
for (int i = 0; i < j; i++) {
/* if (str.charAt(i) != str.charAt(j)) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}*/
/*if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
start = i;
}*/
if (str.charAt(i) == str.charAt(j) &&
((j - i < 3) || dp[i + 1][j - 1])) {
dp[i][j] = true;
if (j - i + 1 > maxLen) {
maxLen = j - i + 1;
start = i;
}
}
}
}
return str.substring(start, start + maxLen);
}
3、两个子序列
3.1 最长公共子序列
// ' a d c e
// ' 0 0 0 0 0
// a 0 1 1 1 1
// c 0 1 1 2 1
public static int longestCommonSubsequence(String s1, String s2) {
int len1 = s1.length();
int len2 = s2.length();
int[][] dp = new int[len1+1][len2+1];
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
if (s1.charAt(i-1) == s2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1]+1;
}else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[len1][len2];
}
4、背包问题
4.1 背包价值最大
先得到该问题的局部解然后扩展到全局问题解。
我们可以假设一个B(k,C) 方法,第k件物品,当前背包所剩下的容量C(初始则C=W)情况下,能够获取的最大价值量。
/**
* 动态规划
* 时间复杂度: O(n * C) 其中n为物品个数; C为背包容积
* 空间复杂度: O(n * C)
*/
public class Solution02 {
public int knapsack(int[] w, int[] v, int C) {
int n = w.length;
int[][] memo = new int[n][C + 1];
if (n == 0 || C == 0)
return 0;
for (int j = 0; j <= C; j++)
memo[0][j] = (j >= w[0] ? v[0] : 0);
for (int i = 1; i < n; i++) {
for (int j = 0; j <= C; j++) {
memo[i][j] = memo[i - 1][j];
if (j >= w[i]) {
memo[i][j] = max(memo[i][j], v[i] + memo[i - 1][j - w[i]]);
}
}
}
return memo[n - 1][C];
}
private int max(int a, int b) {
return a > b ? a : b;
}
public static void main(String[] args) {
int[] w = {1, 2, 3};
int[] v = {6, 10, 12};
int C = 5;
System.out.println(new Solution02().knapsack(w, v, C));
}
}
public static int backPackII(int[] A,int[] V, int m){
int[][] dp = new int[A.length+1][m+1];
for (int i = 1; i <= A.length; i++) {
for (int j = 0; j <= m; j++) {
dp[i][j] = dp[i-1][j];
if (j >= A[i-1] ){
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-A[i-1]] + V[i-1]);
}
}
}
return dp[A.length][m];
}
4.2 背包装最满值
0 1 2 3 4 5 6 7 8 9 10 包大小
2 0 0 2 2 2 2 2 2 2 2 2
3 0 0 2 3 3 5 5 5 5 5 5
5 0 0 2 3 3 5 5 7 8 8 10
物品
// 注意点:每个物品只能选一次,不能重复选择同一个
public static int backPack(int[] A, int m) {
int[] dp = new int[m + 1];
for (int i = 0; i < A.length; i++) {
for (int j = m; j >= A[i]; j--) {
dp[j] = Math.max(dp[j], dp[j-A[i]] + A[i]);
}
}
return dp[m];
}
4.3 找零钱最小个数
public static int coinChange(int[] coins, int amount){
if (coins.length == 0 || amount == 0){
return 0;
}
int[] dp = new int[amount+1];
for (int i = 0; i <= amount ; i++) {
dp[i] = amount+1;
}
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (i >= coins[j]){
dp[i] = Math.min(dp[i-coins[j]]+1, dp[i]);
}
}
}
if (dp[amount] > amount){
return -1;
}
return dp[amount];
}